Skip to content

[SPARK-58263][CORE] Concurrently schedule pipelined-shuffle stage groups in the DAGScheduler - #57341

Open
jerrypeng wants to merge 30 commits into
apache:masterfrom
jerrypeng:stack/pipelined-shuffle-pr3-scheduling
Open

[SPARK-58263][CORE] Concurrently schedule pipelined-shuffle stage groups in the DAGScheduler#57341
jerrypeng wants to merge 30 commits into
apache:masterfrom
jerrypeng:stack/pipelined-shuffle-pr3-scheduling

Conversation

@jerrypeng

@jerrypeng jerrypeng commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

This PR adds native DAGScheduler support for concurrently scheduling stages connected by a
PipelinedShuffleDependency
(added earlier in the stack), together with the admission and
completion semantics that make co-scheduling correct.

A pipelined shuffle is incrementally readable: a consumer stage may begin reading the producer's
output while the producer is still running. The stock scheduler runs a consumer only after its
producer has fully materialized; this PR teaches the scheduler to co-schedule a producer and its
pipelined consumer as a pipelined group instead. Every new path is gated on a job actually
using a pipelined dependency, so behavior is unchanged for all existing jobs (the existing
DAGSchedulerSuite is unaffected).

This PR supports a job that is all-regular or all-pipelined (a job mixing the two is rejected
up front). So an all-pipelined job's whole stage graph is one pipelined group, which lets admission
be decided once, up front.

  • Up-front gang admission (handleJobSubmitted). All members of a pipelined group must run
    concurrently, so a group that cannot fit would deadlock (a consumer holds slots waiting for
    producer output while the producer cannot get slots to produce). Before any stage is created, the
    group's total task demand (computed from the RDD graph) is compared against the currently free
    slots of its resource profile -- total capacity (maxNumConcurrentTasks) minus the outstanding
    (running plus enqueued) task demand of other work in the same profile. Counting enqueued, not
    just running, demand prevents two groups from each passing the check yet failing to co-fit. If the
    group does not fit, the job fails fast with CONCURRENT_SCHEDULER_INSUFFICIENT_SLOT before any
    member runs -- true all-or-nothing admission that leaves no partial scheduler state (like the
    barrier slot check). There is no scheduler-side retry: a transient shortfall is the caller's to
    retry (a streaming query's batch loop reruns the batch). The check can be disabled with
    spark.scheduler.pipelinedGroup.slotCheck.enabled=false for deployments that admit capacity
    out-of-band (e.g. a slot reservation).

  • Co-scheduling (submitStage). A stage's missing parents are classified by shuffle dependency
    type; a parent reached through a PipelinedShuffleDependency is a "pipelined parent". The stage is
    co-scheduled with its producers (tasks submitted immediately) only if every missing parent is
    pipelined and each is already running; otherwise it parks in waitingStages exactly as before.
    submitWaitingPipelinedChildStages is the "producer started running" analog of the existing
    "producer completed" hook: when a pipelined producer starts, its waiting consumers are reconsidered
    immediately, cascading transitively down a chain.

  • Deferred completion for a co-scheduled consumer (handleTaskCompletion /
    markStageAsFinished).
    A consumer co-scheduled with a still-running producer can finish first.
    Advancing its stage/job completion early would end the job and cancel the still-running producer,
    or make the consumer's output observable before the producer's. So the consumer's completion is
    deferred until the producer's outcome is final: its whole CompletionEvent is buffered and
    returns before any side effect runs (accumulator updates, SparkListenerTaskEnd, stage/job
    completion), which makes those side effects run exactly once, at replay. The buffered event is
    replayed on genuine producer success (applied normally), or dropped on producer failure -- on the
    drop the buffered tasks' TaskEnd events are still emitted (the tasks did finish, so active-task
    listeners must see them end) but no stage/job success is applied, since the group reruns. Deferrals
    are cleaned up on job end/abort so none outlive their job, flushing any still-buffered TaskEnd
    events on the way out.

  • Other guards. A job using a pipelined dependency is rejected up front when speculation is
    enabled (a speculative producer copy would race a consumer already reading partial output, with no
    commit barrier), when dynamic allocation is enabled (gang admission needs a stable slot set), or
    when a group member carries a non-default resource profile (gang admission measures capacity
    against the default profile, so the whole group must run on it -- per-profile admission is a
    follow-up); and a PipelinedShuffleDependency cannot be submitted as a map-stage job (no durable
    map output to compute statistics from).

Main changes:

  • DAGScheduler.scala -- job classification, up-front gang admission, co-scheduling, and
    completion deferral. A one-pass RDD-graph walk (rddGraphHasPipelinedDependency /
    classifyJobShuffleKinds) keeps every new path inert for a job with no pipelined dependency.
  • TaskSchedulerImpl.outstandingTasksForOtherWorkInProfile -- a resource-profile-scoped
    outstanding-task (running + enqueued) count for the admission check (private[scheduler]).
  • spark.scheduler.pipelinedGroup.slotCheck.enabled -- a new internal() config (default true).
  • CONCURRENT_SCHEDULER_INSUFFICIENT_SLOT -- a new error condition.

This is a follow-up to PipelinedShuffleDependency and the dependency-typed shuffle-manager
routing (SPARK-58185, already in master); it is the first PR that makes the scheduler behave
differently for a pipelined dependency. Group-atomic failure/rerun and additional fail-fast checks
for unsupported idioms follow in later PRs of the stack.

Why are the changes needed?

PipelinedShuffleDependency and its incremental shuffle routing let a consumer read a producer's
output as it is produced, but nothing takes advantage of that until the scheduler co-schedules the
two stages -- otherwise the consumer still waits for the producer to fully materialize and the
pipelining is never realized. Co-scheduling in turn requires admission control (a group that cannot
co-fit must fail fast, not deadlock) and completion control (a fast-finishing consumer must not end
the job or cancel its producer). This PR provides both.

Does this PR introduce any user-facing change?

No. All new behavior is gated on a job using a PipelinedShuffleDependency, which nothing constructs
yet, so for every existing job the scheduler behaves exactly as before. The new
spark.scheduler.pipelinedGroup.slotCheck.enabled config is internal() and defaults to true,
and CONCURRENT_SCHEDULER_INSUFFICIENT_SLOT is a new error condition that can only surface for a job
that uses a pipelined dependency.

How was this patch tested?

New unit tests in DAGSchedulerSuite cover:

  • concurrent submission of a pipelined producer/consumer; inertness for a regular shuffle; a job
    mixing pipelined + regular shuffles rejected up front; a deep all-pipelined chain co-scheduled;
    transitive cascade when a producer starts; no double-submission on producer completion;
  • speculation, dynamic-allocation, and non-default-resource-profile rejection for a pipelined job
    (and that the corresponding regular jobs are not rejected -- including a regular job that merely
    attaches a non-default profile via RDD.withResources); a pipelined dependency submitted as a
    map-stage job rejected;
  • up-front admission: a group too large to co-fit failing fast with
    CONCURRENT_SCHEDULER_INSUFFICIENT_SLOT; a group that fits total capacity but not free slots
    failing fast; a group whose demand exactly equals free capacity admitted; other work's outstanding
    demand charged against admission; the slot check disabled admitting an over-capacity group;
  • deferred completion: an early-finishing consumer not ending the job or cancelling its running
    producer; its completion buffered until the producer finishes, then applied exactly once at replay
    (no buffer+replay TaskEnd duplication); normal producer-then-consumer ordering; the deferral
    dropped on producer failure; the deferral released only when the producer is genuinely available;
    an explicit job cancellation cleaning up the buffered deferral; and job teardown flushing a
    buffered consumer's TaskEnd events even when the release path never drained it (so active-task
    listeners do not leak the tasks as running).

TaskSchedulerImplSuite covers outstandingTasksForOtherWorkInProfile counting running + enqueued
tasks, excluding given stages, being resource-profile-scoped, and not double-counting a
zombie + live attempt.

Was this patch authored or co-authored using generative AI tooling?

Co-authored with: Claude Code (Opus 4.8)

@jerrypeng
jerrypeng force-pushed the stack/pipelined-shuffle-pr3-scheduling branch from 9aefc6d to f7a6c5e Compare July 17, 2026 21:54
@jerrypeng jerrypeng changed the title [WIP] Stack/pipelined shuffle pr3 scheduling [WIP] [SPARK-XXXXX][CORE] Concurrently schedule pipelined-shuffle stage groups in the DAGScheduler Jul 17, 2026
@jerrypeng
jerrypeng force-pushed the stack/pipelined-shuffle-pr3-scheduling branch 2 times, most recently from 253308e to 4df7ef5 Compare July 22, 2026 18:23
// Update rdd blocks' visibility status.
blockManagerMaster.updateRDDBlockVisibility(
event.taskInfo.taskId, visible = event.reason == Success)
// Group-observable completion for a pipelined consumer: its per-task side effects (accumulator

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic here can be greatly simplified if we do eagerly emit task completion events and delay stage / job completion events.

@jerrypeng jerrypeng changed the title [WIP] [SPARK-XXXXX][CORE] Concurrently schedule pipelined-shuffle stage groups in the DAGScheduler [WIP] [SPARK-58263][CORE] Concurrently schedule pipelined-shuffle stage groups in the DAGScheduler Jul 22, 2026
@jerrypeng jerrypeng changed the title [WIP] [SPARK-58263][CORE] Concurrently schedule pipelined-shuffle stage groups in the DAGScheduler [SPARK-58263][CORE] Concurrently schedule pipelined-shuffle stage groups in the DAGScheduler Jul 22, 2026
@jerrypeng
jerrypeng force-pushed the stack/pipelined-shuffle-pr3-scheduling branch from 4df7ef5 to 89b29a8 Compare July 22, 2026 23:47
@jerrypeng

Copy link
Copy Markdown
Contributor Author

@cloud-fan @mridulm please review.

@cloud-fan cloud-fan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 blocking, 0 non-blocking, 0 nits.
The scheduling and completion design is well covered, but the admission check can consult the wrong capacity for jobs using custom resource profiles.

Correctness (1)

  • core/src/main/scala/org/apache/spark/scheduler/DAGScheduler.scala:1557: Use the pipelined stages' actual resource profile for admission, or reject groups that are not single-profile. This currently always measures the default profile, but stage construction derives each stage's profile from its RDDs. A pipelined job using a custom ResourceProfile can therefore pass admission based on unrelated default-profile capacity and then queue or deadlock in the profile where its tasks actually run. Please derive and validate the group profile before comparing demand and occupancy, and add a custom-profile test. -- see inline

Verification

Traced admission from handleJobSubmitted through rejectUnadmittablePipelinedGroup, then compared its fixed default-profile lookup with createShuffleMapStage/createResultStage, which derive stage profiles from RDD resource profiles. Also audited deferred completion ordering, task-side-effect replay guards, task-set occupancy accounting, configuration inversion coverage, and all three scanner manifests.

if (!sc.conf.get(config.PIPELINED_GROUP_SLOT_CHECK_ENABLED)) {
return false
}
val rp = sc.resourceProfileManager.defaultResourceProfile

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please admit against the pipelined stages' actual resource profile, or reject groups that are not single-profile. Stage construction derives profiles from the RDD graph, so a custom-profile job can pass this default-profile check using unrelated capacity and then queue or deadlock where its tasks actually run. A focused custom-profile test would cover the mismatch.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Let me reject job / group that contain any stages that carries a non-default resource profile.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also added a test for this

// smaller pool -- and could then queue or deadlock there. Reject such a job up front (before
// any stage is created, like the checks above), rather than admit it against the wrong pool.
var offendingRp: Option[ResourceProfile] = None
traverseRDDGraph(finalRDD) { (rdd, enqueue) =>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we technically merge this traversal with the one that we use the calculate the demand. I'll do it in a subsequent PR.

@jerrypeng
jerrypeng requested a review from cloud-fan July 23, 2026 07:41
@jerrypeng

Copy link
Copy Markdown
Contributor Author

@cloud-fan Thank you for your review. I have address your comments. PTAL.

@cloud-fan cloud-fan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 addressed, 0 remaining, 2 new. (0 newly introduced, 2 late catches.)
0 blocking, 0 non-blocking, 2 nits.

Nits: 2 minor items (see inline comments).

Verification

Traced job classification and admission through handleJobSubmitted, recursive co-scheduling through submitStage, deferred task completion and replay through handleTaskCompletion/markStageAsFinished, and profile-scoped occupancy through TaskSchedulerImpl. Reviewed the focused tests and all three scanner manifests; tests were not run as part of this review.

* the stages in `excludeStageIds`. "Outstanding" is the not-yet-completed demand of each task set
* -- running plus enqueued (`numTasks - tasksSuccessful`) -- not just the tasks actively running,
* so a neighbor's queued backlog is charged against capacity too. Used by the pipelined-group
* slot admission check (see DAGScheduler): it compares the group's demand against the slots free

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the slots left free after accounting for everything else in the SAME resource profile. The current phrasing reverses the relationship by saying other work frees slots.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will fix the wording

// producer/consumer co-scheduling makes speculation unsafe. Reject a pipelined
// dependency submitted as a map-stage job outright, up front, before any stage is created so no
// partial scheduler state is left behind. Inert for a regular ShuffleDependency. (The
// result-job path, handleJobSubmitted, only rejects the speculation case, since a pipelined

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update this contract summary: handleJobSubmitted rejects both speculation and dynamic allocation through rejectUnsupportedPipelinedJob, not only speculation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will fix the working

jerrypeng added a commit to jerrypeng/spark that referenced this pull request Jul 23, 2026
…hole completion event, remove finishOnly

Group-observable completion (spec S5) held a pipelined consumer that finished ahead of its
still-running producer. The previous "fine-grained" model ran the consumer's per-task side
effects (accumulator update, task-end listener event) inline and deferred only the
stage/job-completion decision; making the side effects run exactly once then required a
CompletionEvent.finishOnly flag so the replayed event would skip the already-applied
per-task half, plus a special guard so a replay landing on a torn-down stage would not
re-post TaskEnd.

Switch to the simpler coarse model, per review feedback on apache#57341: defer the *whole*
CompletionEvent and return before ANY of its side effects run. This makes the side effects
run exactly once by construction -- at replay, when releaseDeferredPipelinedConsumers
re-posts the buffered event and it re-enters handleTaskCompletion normally -- so:

- CompletionEvent.finishOnly is removed entirely.
- handleTaskCompletion has a single deferral check up front (before updateAccumulators /
  postTaskEnd), and the normal side-effect path is unguarded again (identical to base
  Spark), so the torn-down-stage postTaskEnd guard is no longer needed.
- The producer-failure drop path emits the buffered tasks' TaskEnd events (they genuinely
  succeeded, so active-task-tracking listeners must see them finish) but does NOT run the
  stage/job-completion bookkeeping, since the group is rerun.

Trade-off (accepted): a consumer that finishes early has its task-level listener events
(TaskStart already fired; TaskEnd, accumulator updates) held until the producer's fate is
known, rather than flowing in real time. The stage/job-completion decision -- the part that
would wrongly cancel the still-running producer or expose output early -- is deferred either
way; only the per-task event *timing* differs.

Tests: the two deferral tests now assert the coarse timing (no consumer TaskEnd until the
producer finishes; drop path emits TaskEnd but no result; replay still yields exactly 4
TaskEnd events with no buffer+replay duplication). Reverting the change (emitting TaskEnd
inline before the deferral buffer) makes them fail (TaskEnd count 2 instead of 0 while the
producer runs). Full DAGSchedulerSuite green (344 tests, both variants).

Co-authored-by: Isaac

@cloud-fan cloud-fan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 addressed, 0 remaining, 1 new. (1 newly introduced, 0 late catches.)
0 blocking, 1 non-blocking, 0 nits.

Suggestions (1)

  • core/src/main/scala/org/apache/spark/scheduler/DAGScheduler.scala:2857: Please document the listener-visible consequence of buffering the whole completion event: a successfully finished consumer task remains visible as active in the Spark UI and to SparkListener implementations until its producer finishes. This is a reasonable group-observable contract for the new pipelined feature, but stating it here will keep future maintainers and listener authors from assuming only stage/job completion is deferred. -- see inline

Verification

Statically traced result-job admission through handleJobSubmitted, recursive co-scheduling through submitStage, completion buffering/replay through handleTaskCompletion and markStageAsFinished, teardown through job cleanup, and profile-scoped outstanding demand through TaskSchedulerImpl. Compared the new admission path with barrier scheduling and reviewed all focused tests and scanner manifests. Tests were not run as part of this review.


// Group-observable completion (spec S5): if this stage is a pipelined consumer co-scheduled
// with a still-running pipelined producer, defer its *successful* completion in full until the
// producer(s) finish. Buffer the whole CompletionEvent and return before ANY of its side

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please document that this intentionally delays SparkListenerTaskEnd as well as stage/job completion. Until the producer finishes, the Spark UI and listeners will continue to report successfully completed consumer tasks as active; making that group-observable contract explicit prevents future maintainers and listener authors from assuming that only scheduling completion is deferred.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will reword to make it more clear

@cloud-fan cloud-fan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 addressed, 0 remaining, 1 new. (0 newly introduced, 1 late catch.)
1 blocking, 0 non-blocking, 0 nits.

Correctness (1)

  • core/src/main/scala/org/apache/spark/scheduler/DAGScheduler.scala:3988: Remember a failed producer on the deferral itself until every pending producer has finished. With parents A and B, A can fail first and only be removed from parents; because B remains, no drop occurs and the failure is forgotten. When B later succeeds, the map becomes empty under producerFailed = false and the consumer's buffered successes are replayed even though one producer failed. Please accumulate a failure bit (or drop immediately while retaining TaskEnd cleanup) and add a two-producer test where the first producer fails and the second succeeds. -- see inline

Verification

Traced classification and admission from handleJobSubmitted through resource-profile occupancy, recursive co-scheduling through submitStage, and deferred completion through markStageAsFinished and releaseDeferredPipelinedConsumers. For a consumer waiting on producers A and B, A's failure removes A without storing failure state; B's later success empties the parent set with producerFailed = false and replays the buffered consumer completions. Reviewed the focused tests and all three scanner manifests; tests were not run as part of this review.

if (deferral.parents.isEmpty) {
dependentStageMap -= consumer
val events = deferral.delayedTaskCompletionEvents.toList
if (producerFailed) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preserve a failed-producer outcome until every producer in the set has finished. With parents A and B, A can fail while B remains, so this branch is skipped and A's failure is forgotten; when B later succeeds, its producerFailed = false call empties parents and replays consumer successes from a failed group. Please accumulate failure state in DependentStageInfo and cover the A-fails/B-succeeds ordering with a two-producer test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cloud-fan thanks for pointing this out. What you say is true for this PR but issue is resolved in the next PR . In this PR a pipelined group has no group-atomic failure handling yet: a member failure is routed to a whole-group abort (via isPipelinedGroupMember) only in a later PR of the stack. That abort tears the whole group down synchronously through cancelRunningIndependentStages → markStageAsFinished(Some(reason)) on every member, so once it lands, producer A and producer B both finish as failed together and B never independently reaches the producerFailed = false replay branch. So end-to-end (with the rest of the stack) the buffered successes are not actually replayed.

However, I will add an additional mechanism like the one you mentioned as defense in depth measure here in this PR.

@jerrypeng

Copy link
Copy Markdown
Contributor Author

@cloud-fan thank you for your review. I have addressed your comment. PTAL.

jerrypeng added 11 commits July 25, 2026 23:35
…ined shuffle

Native DAGScheduler support for concurrent-stage scheduling over a
PipelinedShuffleDependency (from the prior PR). A pipelined shuffle is
incrementally readable: its consumer stage may begin reading output while the
producer is still running, so the two are co-scheduled ('pipelined group')
instead of the consumer waiting for the producer to materialize.

submitStage: when a stage has missing parents, classify them by their shuffle
dependency type. A parent read through a PipelinedShuffleDependency is a
pipelined parent. The stage is co-scheduled with its producers (its tasks
submitted immediately) only if every missing parent is pipelined AND each is
already running; otherwise it parks in waitingStages exactly as before. A stage
with a regular missing parent, or a pipelined parent not yet running, waits and
is reconsidered later. This is inert for jobs with no pipelined dependency --
the full DAGSchedulerSuite is unchanged.

submitWaitingPipelinedChildStages: the 'producer started running' analog of
submitWaitingChildStages. When a pipelined producer starts, its waiting
consumers are co-scheduled immediately (not only when the producer completes),
so a consumer parked because its producer sat behind a regular shuffle is
co-scheduled as soon as the producer runs. Cascades transitively.

handleJobSubmitted: reject a job that uses a pipelined dependency when
speculation is enabled -- a speculative producer copy would race a consumer
already reading the producer's partial output, with no commit barrier. The check
runs before stage creation so a rejected job leaves no scheduler state behind.

Tests (DAGSchedulerSuite): concurrent submission, inertness for a regular
shuffle, mixed pipelined+regular parents, a deep all-pipelined chain, a pipelined
producer behind a regular shuffle (must not co-schedule early), two pipelined
parents (co-schedule and re-park semantics), fan-out reconsideration to multiple
consumers, transitive cascade, no double-submission on producer completion, and
speculation rejection (plus that a regular job under speculation is not
rejected).

Co-authored-by: Isaac
…uster capacity

Best-effort admission check for pipelined groups. All member stages of a group
must run concurrently, so if the group's total task demand exceeds the cluster's
total concurrent-task capacity it can never be co-resident and, lacking an
out-of-band slot reservation, would deadlock (the consumer holds slots waiting
for producer output while the producer cannot get slots to produce). We fail
fast with a clear CONCURRENT_SCHEDULER_INSUFFICIENT_SLOT error instead.

When a pipelined group is about to be co-scheduled, submitStage computes the
group's full pipelined-connected component (pipelinedGroupOf) and compares its
summed task demand against maxConcurrentTasksForStage (production:
sc.maxNumConcurrentTasks for the stage's resource profile). If demand exceeds
capacity, the job is aborted; the already-launched producers are torn down by
the normal job-abort path (cancelRunningIndependentStages).

This is deliberately best-effort, not the atomic gang reservation deferred to a
later hardening step: it compares the whole group's demand against TOTAL
capacity (not free slots), checked once at co-schedule time. That converts the
common under-provisioned case (a group that can never fit) into a clear error
rather than a hang; races against other concurrently admitting work are left to
the future atomic version. Inert for jobs with no pipelined dependency.

maxConcurrentTasksForStage is a protected seam so tests can control reported
capacity without changing the cluster's core count.

Tests (DAGSchedulerSuite): a group too large to co-fit fails fast with
CONCURRENT_SCHEDULER_INSUFFICIENT_SLOT; a group that fits is co-scheduled
normally; producer/consumer task sets marked isPipelined; regular task sets are
not.

Co-authored-by: Isaac
…4.1)

Upgrade the gang-admission slot check from total capacity to currently-FREE
slots: free = per-profile total capacity minus tasks already running for OTHER
work (other groups / regular jobs), excluding the group's own already-running
members. Adds TaskSchedulerImpl.runningTasksForOtherWorkInProfile (single-lock,
resource-profile-scoped, zombie-filtered) and a DAGScheduler seam. Comparing
against total capacity would admit a group that fits in principle but cannot
co-fit beside a busy neighbor, then hang; free-slot admission fails fast instead.
Also bumps the incremental-manager config .version to 4.3.0.
… add a slot-check disable flag

Two updates to the pipelined-group slot admission check (spec S4.1), per an updated spec:

- Count outstanding demand, not just running tasks. The occupancy of OTHER work in a resource
  profile now sums each task set's not-yet-completed tasks (numTasks - tasksSuccessful), i.e.
  running plus enqueued, instead of only the tasks actively running. A neighbor's queued backlog is
  real committed demand that can starve a co-scheduled group, so charging only running tasks could
  admit a group that then hangs once the backlog launches. Renamed TaskSchedulerImpl's helper to
  outstandingTasksForOtherWorkInProfile and the DAGScheduler seam to outstandingTasksForOtherWork
  to reflect the new semantics.

- Add spark.scheduler.pipelinedGroup.slotCheck.enabled (internal, default true). When false,
  pipelinedGroupExceedsCapacity is skipped entirely and a group is co-scheduled unconditionally.
  This mirrors ConcurrentStageDAGScheduler's ability to disable its slot check, for deployments that
  admit capacity out-of-band (e.g. a slot reservation) and own admission themselves.

Tests: the TaskSchedulerImpl unit test now submits more tasks than slots and asserts the count
includes the enqueued tasks; a new DAGScheduler test asserts that with the check disabled an
over-capacity group is co-scheduled rather than failed.

Co-authored-by: Isaac
… the group finishes

Group-observable completion for pipelined groups (spec S5). A stage co-scheduled
with a still-running pipelined producer (a pipelined consumer) must not have its
successful completion processed early: doing so would advance job completion and
cancel the still-running producer (via cancelRunningIndependentStages), or make
the consumer's output observable before the producer's.

handleTaskCompletion: near the top, before any of the event's side effects, if a
Success event belongs to a pipelined consumer with unfinished pipelined
producers, buffer the whole CompletionEvent and return. This defers the entire
event (coarse model), so its side effects (accumulator update, TaskEnd listener
event, stage/job completion) run exactly once -- at replay.

markStageAsFinished: when a pipelined producer finishes, release its deferred
consumers -- but only when the producer's outcome is final. A ShuffleMapStage
that finished without error yet is not isAvailable (an output missing; it will be
resubmitted) is NOT treated as done, so its consumers stay deferred until the
reattempt makes it available. On genuine success the buffered events are
replayed; on producer failure they are dropped (the group reruns, S6) but their
TaskEnd events are still emitted so listeners do not leak active-task accounting.

cleanupStateForJobAndIndependentStages: drop any deferral keyed on a removed
stage and remove it from other consumers' pending-producer sets, so no deferral
outlives its job (e.g. on abort). assertDataStructuresEmpty also checks the
deferral map is empty.

Inert for jobs with no pipelined dependency: the deferral map is never populated,
the completion check is a always-miss map lookup, and release/cleanup are no-ops.

Tests (DAGSchedulerSuite): early-finishing consumer does not end the job or
cancel its running producer; normal producer-then-consumer ordering; buffered
completions dropped when the producer fails; TaskEnd fired exactly once (no
buffer+replay duplication); deferral released only when the producer is
genuinely available.

Co-authored-by: Isaac
…ts, reject dynamic allocation

Two correctness fixes to the pipelined-group slot admission, found in review:

- Skip zombie attempts in outstandingTasksForOtherWorkInProfile. The occupancy count summed
  numTasks - tasksSuccessful over every attempt in taskSetsByStageIdAndAttempt, including zombie
  (superseded) attempts. A retried/killed stage can have both a zombie and a live attempt in the
  map at once, and the live attempt already re-runs the zombie's outstanding tasks -- so counting
  both double-counted that stage's demand, inflating occupancy and potentially failing a pipelined
  group with CONCURRENT_SCHEDULER_INSUFFICIENT_SLOT that would actually fit. Add the !isZombie
  filter used elsewhere on this map.

- Reject a pipelined-shuffle job under dynamic allocation. A pipelined group is gang-scheduled and
  its free-slot check measures currently-active executors; under dynamic allocation a job can be
  submitted before any executor has spun up, so the group would be failed against a transient
  0-slot snapshot even though the cluster would soon have capacity. Barrier scheduling forbids the
  same combination (checkBarrierStageWithDynamicAllocation); pipelined groups now do likewise.
  The former rejectSpeculationWithPipelinedShuffle is generalized to rejectUnsupportedPipelinedJob,
  which rejects both speculation and dynamic allocation (single RDD-graph walk, run only when a
  relevant feature is enabled).

Tests: a zombie + live attempt is counted once, not twice; a pipelined job under dynamic allocation
is rejected while a regular job under dynamic allocation is not.

Co-authored-by: Isaac
…by a later PR in the stack

isPipelinedGroupMember is defined here alongside the other group-topology helpers but is first used
by the group-atomic failure handling added in a later PR of this stack (TaskSet.isPipelined and the
member-failure fail-fast, spec S6). Reviewed in isolation this change reads as introducing an unused
private method; note the forward reference in the scaladoc so it is not mistaken for dead code.

Co-authored-by: Isaac
…unning plus enqueued

The admission check counts OTHER work's OUTSTANDING tasks (running plus
enqueued, `numTasks - tasksSuccessful`) against a resource profile's
capacity -- as `outstandingTasksForOtherWork` / `TaskSchedulerImpl.
outstandingTasksForOtherWorkInProfile` and the config doc already state.
Two spots in DAGScheduler still described this as only the tasks "already
running", which was stale after the count was widened to include enqueued
tasks; the `pipelinedGroupExceedsCapacity` scaladoc even contradicted its
own later "running or enqueued" line. Reword both to "outstanding --
running plus enqueued" so the prose matches the code. Doc/comment only; no
behavior change.

Co-authored-by: Isaac
… admit the group up front

v1 (M1, real-time-mode scope) supports a job that is either all-regular or
all-pipelined, not a mix. This lets an all-pipelined job's whole stage graph be
one pipelined group with no regular prefix, so gang admission can be decided up
front -- before any member stage is submitted -- rather than mid-DAG once a
producer is already running.

Changes in handleJobSubmitted (before createResultStage, so a rejection leaves
no partial scheduler state, exactly like the barrier slot check and the
speculation/DA reject):
 - classifyJobShuffleKinds walks the RDD graph and rejects a job that mixes a
   pipelined shuffle with a regular one, fail-fast.
 - rejectUnadmittablePipelinedGroup computes the whole all-pipelined group's
   concurrent-task demand from the RDD graph and checks it against free slots
   (maxNumConcurrentTasks minus other work's running-plus-enqueued demand, spec
   S4.1); fails the job if it cannot fit. No scheduler retry -- a transient
   shortfall is the caller's to retry (e.g. the streaming batch loop reruns the
   batch).

This is true all-or-nothing gang admission: the whole group is admitted, or the
job fails before any member runs, so a member is never left running while a
sibling cannot get slots. The late slot check in submitStage's co-schedule
branch (which measured a mid-flight snapshot after a producer was already
running) is removed, along with the now-unused pipelinedGroupExceedsCapacity /
pipelinedGroupOf; the capacity/occupancy seams are refactored to be
resource-profile-keyed.

Tests: mixed jobs rejected up front; all-PG group admitted/rejected up front
(2- and 3-stage); the removed mid-DAG prefix tests are dropped as that shape is
no longer supported.

Co-authored-by: Isaac
…oncurrentStageDAGScheduler

Rename the deferred-completion machinery to match the production RTM scheduler
(ConcurrentStageDAGScheduler), so reviewers familiar with that code read the
same concepts here. Pure rename; no behavior change.

 - pipelinedConsumerDeferrals -> dependentStageMap
 - DeferredCompletion         -> DependentStageInfo
 - pendingProducers           -> parents
 - bufferedEvents             -> delayedTaskCompletionEvents

The pipelined-specific helpers (isPipelinedProducer, isPipelinedGroupMember,
submitWaitingPipelinedChildStages, rejectUnadmittablePipelinedGroup) keep their
names -- they belong to the type-driven pipelined model and have no clean
analog in the reference scheduler.

Co-authored-by: Isaac
… check; fix a vacuous admission test

Two test-hygiene fixes from an adversarial review:

- assertDataStructuresEmpty now asserts dependentStageMap.isEmpty, per the
  DAGScheduler class-doc checklist ("when adding a new data structure, update
  DAGSchedulerSuite.assertDataStructuresEmpty ... to catch memory leaks"). The
  new dependentStageMap (the pipelined deferred-completion buffer) was omitted;
  it is always drained today, so this passes -- and now guards a future leak of
  buffered CompletionEvents / a consumer stage left as perpetually-running.

- Retitle/reframe "the group's own running members are not charged against its
  admission": it stubbed the occupancy seam to a constant ignoring
  excludeStageIds, and the up-front admission passes Set.empty (no job stage
  exists yet), so it never exercised member exclusion -- it only asserts a
  demand==free-capacity group is admitted (a real boundary). The member-exclusion
  invariant is genuinely covered in TaskSchedulerImplSuite; the title/comment now
  say what the test actually verifies.

Co-authored-by: Isaac
jerrypeng added 15 commits July 25, 2026 23:35
…n, run per-task effects inline (S5.1)

The group-observable completion for a pipelined consumer (spec S5) previously used a
coarse model: when a consumer finished ahead of its still-running producer, the WHOLE
CompletionEvent was buffered and its side effects were withheld until replay. That
contradicts the negotiated spec S5.1, which requires per-task side effects -- accumulator
updates and the SparkListenerTaskEnd listener event -- to flow in real time, deferring
ONLY the stage/job-completion decision (advancing that early is what would cancel the
still-running producer or expose the consumer's output prematurely).

Switch to the fine-grained model:
- handleTaskCompletion runs a deferred consumer's per-task effects inline (as for any
  other stage), then buffers the event and returns before the completion bookkeeping.
- A new CompletionEvent.finishOnly flag marks a replayed event; on replay only the
  completion bookkeeping runs, so the per-task effects are never applied twice.
- releaseDeferredPipelinedConsumers replays with finishOnly=true; the producer-failure
  drop path simply discards the buffered events (their TaskEnd already fired inline),
  removing the old asymmetry where the drop path re-emitted postTaskEnd but not
  updateAccumulators.

Observability improves: a finished consumer's tasks are no longer reported as running for
the producer's remaining lifetime, and no late TaskEnd burst appears at group abort.

Tests: the two deferral tests now assert per-task TaskEnd fires in real time (before the
producer finishes) and exactly once. Reverting the fix makes them fail (TaskEnd count 0
instead of 2 while the producer runs). Full DAGSchedulerSuite green.

Co-authored-by: Isaac
…reads as unused" and a phantom symbol

The NOTE on isPipelinedGroupMember said it "reads as unused when this change is viewed
alone" and listed `pipelinedGroupOf` as a sibling helper. Both are inaccurate in the
merged view: the method IS used (its call sites -- TaskSet.isPipelined tagging and the
member-FetchFailed group abort -- are part of this stack), and `pipelinedGroupOf` does not
exist. Reword to name the actual call sites and drop the phantom reference.

Co-authored-by: Isaac
…private[scheduler]

The method exists only to back the pipelined-group slot admission check in DAGScheduler; it
does not belong on the public TaskSchedulerImpl surface. Its only callers (DAGScheduler and
TaskSchedulerImplSuite) are in org.apache.spark.scheduler, so private[scheduler] is
sufficient and keeps the new capacity-accounting method out of the public API.

Co-authored-by: Isaac
… the retry behavior

The gang-admission scaladoc likened the check to barrier's slot check. The likeness is only
that both reject before any stage is created and compute demand from the RDD graph; their
retry behavior differs and a reader should not infer equivalence. Spell out the contrast:
barrier re-posts the job and retries its check up to a max-failures bound, whereas pipelined
admission is terminal (one check, then fail) and delegates transient-shortfall retry to the
caller -- scheduler-side PG-admission retry being a post-v1 refinement (spec S4.1).

Co-authored-by: Isaac
…t lands on a torn-down stage

A deferred pipelined-consumer completion fires its TaskEnd inline on first completion; only
the stage/job bookkeeping is re-posted (finishOnly=true) once the producer finishes. The
cancelled-stage guard in handleTaskCompletion (which posts TaskEnd for an event whose stage
is no longer tracked) ran unconditionally and before the finishOnly check. If the group is
torn down (a sibling aborts) between a finishOnly re-post and its dequeue, the replay lands
on the removed stage and the guard would re-post TaskEnd -- double-counting it in listeners
(AppStatusListener's active-task count could go negative). Skip postTaskEnd for a finishOnly
event in that guard: its per-task TaskEnd already fired inline.

A regression test on the failure PR (which has the group-atomic abort this needs) drives a
finishOnly replay onto a torn-down stage and asserts no extra TaskEnd; reverting this guard
makes it fail (TaskEnd count goes up by one).

Co-authored-by: Isaac
…cumulator deltas are not un-merged

The drop-path comment implied full cleanliness ("only the deferred completion bookkeeping
was buffered ... must NOT be applied"). But a deferred consumer's per-task effects, including
updateAccumulators, run inline before buffering, so on a producer-failure drop the already-
applied accumulator deltas are NOT un-merged. Note that this is consistent with base Spark
(accumulators are non-transactional across abort+rerun): the rerun re-delivers under the S5
idempotent-sink contract, and RTM SQL metrics are fresh per batch, so there is no cross-batch
double-count. Comment-only.

Co-authored-by: Isaac
…ments

Strip internal shorthand that a reader of merged Apache Spark cannot resolve
-- version/milestone tags, design-doc spec section numbers, and
fine-grained/coarse model terminology -- from the pipelined-shuffle comments,
one test title, and the mixed-job error message, rewording to plain behavioral
language. Comment, test-title, and error/assertion-string text only; no
behavioral change.

Co-authored-by: Isaac
…fault resource profile

Up-front gang admission measures capacity and occupancy against the default
resource profile, but each stage derives its profile from its RDDs. A pipelined
job whose members use a non-default resource profile could therefore pass
admission against the default profile's free slots and then queue or deadlock in
the profile where its tasks actually run.

Reject such a job up front in rejectUnadmittablePipelinedGroup (before any stage
is created, like the other up-front rejections): walk the group's RDD graph and
fail the job if any member carries a non-default resource profile. The whole
pipelined group is required to run on the default profile -- the assumption the
admission check already relies on. Per-profile admission is a follow-up.

Add a DAGSchedulerSuite test: a pipelined consumer on a custom resource profile
is rejected before any stage is created.

Co-authored-by: Isaac
…hole completion event, remove finishOnly

Group-observable completion (spec S5) held a pipelined consumer that finished ahead of its
still-running producer. The previous "fine-grained" model ran the consumer's per-task side
effects (accumulator update, task-end listener event) inline and deferred only the
stage/job-completion decision; making the side effects run exactly once then required a
CompletionEvent.finishOnly flag so the replayed event would skip the already-applied
per-task half, plus a special guard so a replay landing on a torn-down stage would not
re-post TaskEnd.

Switch to the simpler coarse model, per review feedback on apache#57341: defer the *whole*
CompletionEvent and return before ANY of its side effects run. This makes the side effects
run exactly once by construction -- at replay, when releaseDeferredPipelinedConsumers
re-posts the buffered event and it re-enters handleTaskCompletion normally -- so:

- CompletionEvent.finishOnly is removed entirely.
- handleTaskCompletion has a single deferral check up front (before updateAccumulators /
  postTaskEnd), and the normal side-effect path is unguarded again (identical to base
  Spark), so the torn-down-stage postTaskEnd guard is no longer needed.
- The producer-failure drop path emits the buffered tasks' TaskEnd events (they genuinely
  succeeded, so active-task-tracking listeners must see them finish) but does NOT run the
  stage/job-completion bookkeeping, since the group is rerun.

Trade-off (accepted): a consumer that finishes early has its task-level listener events
(TaskStart already fired; TaskEnd, accumulator updates) held until the producer's fate is
known, rather than flowing in real time. The stage/job-completion decision -- the part that
would wrongly cancel the still-running producer or expose output early -- is deferred either
way; only the per-task event *timing* differs.

Tests: the two deferral tests now assert the coarse timing (no consumer TaskEnd until the
producer finishes; drop path emits TaskEnd but no result; replay still yields exactly 4
TaskEnd events with no buffer+replay duplication). Reverting the change (emitting TaskEnd
inline before the deferral buffer) makes them fail (TaskEnd count 2 instead of 0 while the
producer runs). Full DAGSchedulerSuite green (344 tests, both variants).

Co-authored-by: Isaac
…own, not just on the release path

The coarse deferral model buffers a pipelined consumer's whole completion event -- including its
TaskEnd -- until group completion. releaseDeferredPipelinedConsumers flushes those buffered
TaskEnds on both its replay (producer succeeded) and drop (producer failed) paths, and it is
normally reached for every producer before job cleanup runs (cancelRunningIndependentStages
finishes each running producer first).

But cleanupStateForJobAndIndependentStages -> removeStage discarded a consumer's deferral with a
bare `dependentStageMap -= stage`, without flushing its buffered TaskEnds. That silently loses
them in a case the release path does not cover: a producer left in resubmit limbo -- finished
with no error yet not available, so producerAboutToResubmit retained the deferral AND
markStageAsFinished removed the producer from runningStages -- is skipped by
cancelRunningIndependentStages (it is neither running nor failed). If the job is then cancelled
or aborted, cleanup drops the consumer's buffered completion without ever emitting its TaskEnds,
so a listener that tracks active tasks (e.g. AppStatusListener) leaks them as perpetually
running. This was impossible before the coarse-model change, when TaskEnd fired inline on first
completion and nothing was buffered to lose.

Fix: at removeStage, drain the removed consumer's buffered delayedTaskCompletionEvents through
postTaskEnd (as the producer-failed drop path does) before discarding the deferral. The buffered
successes are still not applied as results -- this is a job failure / cancellation -- only their
TaskEnds are flushed.

Test: a regression test drives a producer into resubmit limbo, cancels the job, and asserts the
2 buffered consumer TaskEnds are flushed (and no result applied). Reverting the fix makes it fail
(0 TaskEnds instead of 2). Full DAGSchedulerSuite green.

Co-authored-by: Isaac
Two documentation-only corrections from review (cloud-fan), no behavior change:

- TaskSchedulerImpl.outstandingTasksForOtherWorkInProfile scaladoc: "the slots free by everything
  else" reversed the relationship (other work consumes slots, it does not free them). Reword to
  "the slots left free after accounting for everything else in the SAME resource profile".

- DAGScheduler.handleMapStageSubmitted comment: the parenthetical said the result-job path
  (handleJobSubmitted) "only rejects the speculation case". It rejects both speculation and dynamic
  allocation via rejectUnsupportedPipelinedJob; correct the summary.

Co-authored-by: Isaac
…test assert

The assert message in "job teardown flushes a buffered consumer's TaskEnds" was
101 chars, over the 100-char scalastyle limit for test sources. Trim it.

Co-authored-by: Isaac
… per-task side effects

Address review (cloud-fan): make the group-observable-completion comment explicit that
buffering the whole CompletionEvent DEFERS all three side effects -- the accumulator update,
the task-end listener event, and stage/job completion -- not just the stage/job-completion
bookkeeping. Also state the listener-visible consequence: a consumer task that finishes ahead
of its producer emits no SparkListenerTaskEnd until the producer finishes, so the Spark UI
reports that already-succeeded task as still running for the producer's remaining lifetime.

Comment-only; no behavior change.

Co-authored-by: Isaac
…producer fails

releaseDeferredPipelinedConsumers decided drop-vs-replay only when a consumer's last pending
producer finished, keyed on that producer's outcome. For a consumer co-scheduled with more than
one pipelined producer (fan-in), producer A could fail while B was still pending -- A was removed
from the pending set but no drop occurred, and A's failure was forgotten -- so when B later
succeeded it emptied the set on the replay branch and the consumer's buffered successes were
replayed even though they were computed against A's now-invalid output.

Fix: when a producer fails, drop the consumer's deferral immediately -- flush its buffered
TaskEnds and remove the entry -- regardless of whether it still has other pending producers. A
pipelined group is failed as a unit (S6), so once any producer fails the buffered successes must
not be applied. Dropping now (rather than remembering the failure until the last producer
finishes) also leaves no deferral state that could outlive the call and be reused by a later
re-co-scheduling of the same stage. A surviving producer that finishes afterward simply finds the
consumer already gone (a clean no-op). Producer success is unchanged: remove the finished producer
and replay only once the last one has succeeded.

Recovery from a drop, if any, is a caller rerun of the whole group (a new job, S6); the scheduler
does not rerun the group in place.

Test: a two-producer (fan-in) case where A fails and B then succeeds, asserting the consumer's
deferral is dropped as soon as A fails and B's later success does not replay it. Reverting to the
old parents-empty gating makes it fail (the successes replay). Full DAGSchedulerSuite green.

Co-authored-by: Isaac
… drop path

The immediate-drop on producer failure treats producerFailed as terminal. That holds for every
producer-failure path that reaches releaseDeferredPipelinedConsumers except one: a barrier stage
that fails a task and is then resubmitted (its handler calls markStageAsFinished(errorMessage)
without willRetry=true, then resubmits). For a fan-in consumer, dropping its deferral for a barrier
producer that will retry loses the consumer's buffered results.

A barrier stage in a pipelined group is rejected up front by a later PR in this stack
(checkPipelinedProducerSupported), which removes this path entirely; on this PR standalone it is a
known, narrow limitation (needs a barrier producer feeding a fan-in consumer). Document it inline
rather than adding the rejection early.

Comment-only; no behavior change.

Co-authored-by: Isaac
@jerrypeng
jerrypeng force-pushed the stack/pipelined-shuffle-pr3-scheduling branch from 787869b to 80b8aa8 Compare July 25, 2026 23:46
…sition-agnostic

The fan-in drop-path caveat said the barrier-resubmit edge is closed "by a later PR in this
stack (checkPipelinedProducerSupported) ... on this PR standalone it is a known limitation."
That reads as stale once the commit is cascaded onto the PR that actually adds
checkPipelinedProducerSupported (the method is then present, not in a "later PR"). Reword so it
is accurate regardless of which stacked PR you view it from: the edge is closed by rejecting a
barrier stage in a pipelined group up front (checkPipelinedProducerSupported, added with the
group fail-fast checks later in the stack), and until that rejection is in place it is a known,
narrow limitation.

Comment-only; no behavior change.

Co-authored-by: Isaac

@cloud-fan cloud-fan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 addressed, 0 remaining, 2 new. (1 newly introduced, 1 late catch, 0 previously raised.)
1 blocking, 0 non-blocking, 1 nit.
The prior fan-in failure issue is fixed, but a barrier-producer retry can still discard the consumer's deferred completions and prevent the job from completing.

Correctness (1)

  • core/src/main/scala/org/apache/spark/scheduler/DAGScheduler.scala:3996: A barrier shuffle-map producer can fail a task and be resubmitted, but markStageAsFinished is called without willRetry=true. The new release path therefore treats the attempt as terminal and drops the already-successful consumer completions. The producer retry can then succeed with no buffered consumer events left to complete the job. Reject barrier members up front in this PR, or retain the deferral across the retry, and add the focused barrier-producer retry test. -- see inline

Nits: 1 minor item (see inline comments).

Verification

Traced admission from handleJobSubmitted through profile-scoped outstanding-task accounting, recursive co-scheduling through submitStage, and deferred completion through handleTaskCompletion and markStageAsFinished. Verified the remaining failure path in the barrier-task handler: it calls markStageAsFinished with an error and then resubmits the stage, while releaseDeferredPipelinedConsumers treats that call as terminal and drops the consumer buffer. Tests were not run as part of this review.

// now leaves no deferral state to go stale for a later re-co-scheduling of this stage.
// This treats producerFailed as terminal, which holds for every producer-failure path that
// reaches here EXCEPT a barrier stage that fails a task and is resubmitted (its handler
// calls markStageAsFinished(errorMessage) without willRetry=true, then resubmits):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please close this retry path in this PR. A barrier producer failure reaches markStageAsFinished(..., Some(message)) and is then resubmitted, but this branch treats the attempt as terminal and drops the already-successful consumer completions. When the producer retry succeeds, nothing remains to complete the consumer job. Reject barrier members up front or preserve the deferral across the retry, and cover that ordering with a focused test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The plan was to add the rejection of barrier stages in the next PR that focuses on rejection of mechanism not supported with PGs as the comment documents. Though I can added it in this PR if you think it is more appropriate.


/**
* Deferred completion for pipelined groups. When a stage is co-scheduled with a pipelined
* producer that is still running (a "pipelined consumer"), the stage/job-completion decision from

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move (a "pipelined consumer") after stage; here it reads as though the still-running producer is the consumer.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will reword

…ront

Address review (cloud-fan, blocking): a barrier shuffle-map producer can fail a task and be
resubmitted (its handler calls markStageAsFinished(errorMessage) without willRetry=true, then
resubmits). The deferred-completion drop path treats that failure as terminal and drops a
co-scheduled consumer's buffered completions; when the producer's retry then succeeds, nothing
remains to complete the consumer's job, so it hangs.

Close the path up front rather than document it: reject any job whose pipelined group contains a
barrier member before any stage is created, in rejectUnadmittablePipelinedGroup (the same up-front
reject that already handles a non-default resource profile), folded into the one RDD-graph walk. A
barrier stage exposes its output only after a global sync, which is incompatible with a pipelined
consumer reading it incrementally anyway. With the rejection in place, a barrier member can never
be co-scheduled, so the resubmit path is unreachable and treating producerFailed as terminal is
sound; the drop-path comment is updated to state this instead of describing a known limitation.

Test: a barrier producer feeding a pipelined consumer is rejected before any stage is created.
Reverting the rejection makes it fail (the job is not rejected). Full DAGSchedulerSuite green.

Co-authored-by: Isaac
…d consumer" to the stage

Address review nit (cloud-fan): the parenthetical (a "pipelined consumer") sat after "a pipelined
producer that is still running", so it read as though the producer is the consumer. Move it to
follow "stage" -- the co-scheduled stage is the consumer.

Comment-only; no behavior change.

Co-authored-by: Isaac
@jerrypeng

Copy link
Copy Markdown
Contributor Author

@cloud-fan thank you for your review. I have addressed your comment. PTAL.

@cloud-fan cloud-fan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 addressed, 0 remaining, 1 new. (0 newly introduced, 1 late catch, 0 previously raised.)
1 blocking, 0 non-blocking, 0 nits.
The prior issues are addressed, but admission and execution use different identities for the same pipelined group, which can make a valid group fail admission.

Correctness (1)

  • core/src/main/scala/org/apache/spark/scheduler/DAGScheduler.scala:1529: Make admission use the same identity as stage construction. Execution creates one ShuffleMapStage per shuffle ID, but pipelinedJobConcurrentTaskDemand charges each dependency edge. A diamond or fan-out graph that references one PipelinedShuffleDependency from two downstream RDDs therefore counts one producer twice and can reject a group that actually fits. Track visited pipelined shuffle IDs while computing demand. Cover both sides of the identity contract: repeated edges to one dependency count once, while two distinct shuffle IDs over the same producer RDD count twice. -- see inline

Verification

Statically traced RDD-graph traversal, stage creation keyed by shuffle ID, admission accounting, recursive submission, completion deferral/replay, and teardown. Reviewed the focused scheduler tests and all three scanner manifests; tests were not run as part of this review.

var demand = finalNumPartitions
traverseRDDGraph(finalRDD) { (rdd, enqueue) =>
rdd.dependencies.foreach {
case pd: PipelinedShuffleDependency[_, _, _] => demand += pd.rdd.partitions.length

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make admission count the same identities that execution schedules. getOrCreateShuffleMapStage creates one stage per shuffle ID, but this increment runs per dependency edge, so a diamond or fan-out graph that references one PipelinedShuffleDependency twice can be rejected even when its actual stages fit. Track visited pipelined shuffle IDs here, and test both that repeated edges to one dependency count once and that distinct shuffle IDs over the same producer RDD still count separately.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good catch! I have created a fix that tracks visited pipelined shuffle IDs and added more testing in this area. PTAL

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about this some more, the reason why I didn't implement the check to begin with is that I am not planning on diamond or fan-out is not supported for PGs for this initial version because streaming shuffle doesn't even support such operations. The actual rejection logic is in the next PR. I can still include the mechanism just to be safe but in reality it will not be necessary for this version.

… ID, not per edge

pipelinedJobConcurrentTaskDemand walked the RDD graph and added a pipelined producer's
task count once per dependency EDGE, but execution creates one stage per shuffle ID
(getOrCreateShuffleMapStage). A fan-out or diamond graph references a single
PipelinedShuffleDependency from more than one consumer RDD, so the per-edge count
over-inflated demand and could fail an all-pipelined group up front
(CONCURRENT_SCHEDULER_INSUFFICIENT_SLOT) even though its actual stages would co-fit.

Track visited pipelined shuffle IDs and charge each producer once. Dedup on shuffleId,
not pd.rdd: two distinct PipelinedShuffleDependency objects can share a producer RDD yet
carry distinct shuffle IDs and produce distinct stages, so both must be counted -- keying
on the RDD would undercount and admit a group that does not fit.

Tests (DAGSchedulerSuite), covering a variety of graph shapes:
- a diamond that reuses one producer across two consumers counts it once (admitted at the
  capacity that per-edge counting would have rejected);
- a wider fan-out (one producer, three consumers) counts it once;
- two distinct shuffles over the same producer RDD are both counted (the group is
  correctly rejected -- guards against over-deduping on the producer RDD).
All three were revert-checked: the diamond and fan-out tests fail if the fix is reverted
to per-edge counting; the two-distinct-shuffles test fails if the dedup key is changed
from shuffleId to the producer RDD.

Co-authored-by: Isaac
@jerrypeng

Copy link
Copy Markdown
Contributor Author

@cloud-fan thank you for your review. I have addressed your comment. PTAL.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants